package com.exam.api.common.cardRecognition;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.exam.api.common.exception.GlobalException;
import lombok.extern.log4j.Log4j2;
import org.opencv.core.*;
import org.opencv.highgui.Highgui;
import org.opencv.imgproc.Imgproc;
import org.springframework.stereotype.Component;

import java.io.ByteArrayOutputStream;
import java.util.*;

@Component
@Log4j2
public class CardRecognitionService {
    static {
        nu.pattern.OpenCV.loadLocally();
    }
    public CardRecognitionResult start(ByteArrayOutputStream imageStream) {
        return start(imageStream,75);
    }

    public CardRecognitionResult start(ByteArrayOutputStream imageStream, Integer maxQuestionCount) {
        return start(imageStream,maxQuestionCount,null);
    }

    public CardRecognitionResult start(ByteArrayOutputStream imageStream, Integer maxQuestionCount,String outputFile) {
        //先缩放到固定大小
        Mat resized = resizeImage(imageStream);
        if(StrUtil.isNotEmpty(outputFile)){
            String destPath = outputFile + "s1_resize.png";
            Highgui.imwrite(destPath, resized);
        }
        Mat threshold1 = thresholding1(resized);
        if(StrUtil.isNotEmpty(outputFile)){
            String destPath = outputFile + "s2_threshold1.png";
            Highgui.imwrite(destPath, threshold1);
        }

        //找到答题卡最外层的轮廓
        List<double[]> outsideBorders = findOutsideBorders(threshold1);
        if(StrUtil.isNotEmpty(outputFile)) {
            for (double[] border : outsideBorders) {
                Core.line(threshold1, new Point(border[0], border[1]), new Point(border[2], border[3]), new Scalar(255, 0, 0), 4);
            }
            String destPath = outputFile + "s2_1_border.png";
            Highgui.imwrite(destPath, threshold1);
        }

        Mat perspective = warpPerspective(resized, outsideBorders);
        if(StrUtil.isNotEmpty(outputFile)){
            String destPath = outputFile + "s3_perspective.png";
            Highgui.imwrite(destPath, perspective);
        }

        Mat cut = cutCard(perspective);
        if(StrUtil.isNotEmpty(outputFile)){
            String destPath = outputFile + "s4_cut.png";
            Highgui.imwrite(destPath, cut);
        }

        Mat threshold2 = thresholding2(cut);
        if(StrUtil.isNotEmpty(outputFile)){
            String destPath = outputFile + "s5_threshold2.png";
            Highgui.imwrite(destPath, threshold2);
        }

        CardRecognitionResult cardRecognitionResult = cardResult(cut,threshold2, maxQuestionCount);
        FileUtil.writeBytes(cardRecognitionResult.getResultImageArray(), outputFile + "s6_result.png");
        threshold1.release();
        threshold2.release();
        perspective.release();
        resized.release();
        cut.release();
        return cardRecognitionResult;
    }

    /**
     * 先将原始图片缩放到固定大小
     * @param imageStream
     * @return
     */
    public Mat resizeImage(ByteArrayOutputStream imageStream){
        Mat source = Highgui.imdecode(new MatOfByte(imageStream.toByteArray()), Highgui.CV_LOAD_IMAGE_COLOR);
        if(source.width() > source.height()){
            Core.transpose(source, source);
            Core.flip(source, source, 1);
        }
        Mat resized = new Mat();
        double targetWidth = 1000;
        double targetHeight = source.height() * (targetWidth / source.width());
        Imgproc.resize(source, resized, new Size(targetWidth, targetHeight));
        source.release();
        return resized;
    }

    /**
     * 对整个照片二值化
     * @param source
     * @return
     */
    public Mat thresholding1(Mat source){
        Mat result = new Mat();
        // 彩色转灰度
        Imgproc.cvtColor(source, result, Imgproc.COLOR_BGR2GRAY);
        //此处图像预处理，可以使用方式1也可以使用方式2都可以，自己也可以测试下2种方式的差异
//			//方式1：通过高斯滤波然后边缘检测膨胀来链接边缘，将轮廓连通便于轮廓识别
//			// 高斯滤波，降噪
        Imgproc.GaussianBlur(result, result, new Size(3,3), 1, 1);
        // Canny边缘检测
        Imgproc.Canny(result, result, 50, 150, 3, false);
        // 膨胀，连接边缘
        Imgproc.dilate(result, result, new Mat(), new Point(-1,-1), 1, 1, new Scalar(1));
//
        //方式2:使用形态学梯度算法，此算法来保留物体的边缘轮廓很有效果
//        Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5,5));
//        Imgproc.morphologyEx(img, img, Imgproc.MORPH_GRADIENT, element);

        //TODO:这里的轮廓比较浅
        //图像二值化，使用的是OTSU二值化，阈值为170，也可以使用自适用二值化
//			Imgproc.adaptiveThreshold(img,img, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C,  Imgproc.THRESH_BINARY_INV, 51, 10);
        Imgproc.threshold(result,result, 170, 255, Imgproc.THRESH_BINARY|Imgproc.THRESH_OTSU);

        return result;
    }

    /**
     * 只对答题区域二值化
     * @param source
     * @return
     */
    public Mat thresholding2(Mat source){
        Mat result = new Mat();
        //图像灰度化
        Imgproc.cvtColor(source, result, Imgproc.COLOR_BGR2GRAY);
        //图像二值化，注意是反向二值化以及OTSU算法
//        Imgproc.threshold(img_gray,img_gray, 170, 255, Imgproc.THRESH_BINARY_INV|Imgproc.THRESH_OTSU);
        //此处使用的是方式2，形态学梯度算法保留填图选项的边框
//		 	//方式1：通过高斯滤波然后边缘检测膨胀来链接边缘，将轮廓连通便于轮廓识别
//			// 高斯滤波，降噪
        Imgproc.GaussianBlur(result, result, new Size(1,1), 2, 2);
        // Canny边缘检测
        Imgproc.Canny(result, result, 50, 90, 3, false);
        // 膨胀，连接边缘
        Imgproc.dilate(result, result, new Mat(), new Point(-1,-1), 1, 1, new Scalar(1));

        //方式2:使用形态学梯度算法，此算法来保留物体的边缘轮廓很有效果
//        Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5,5));
//        Imgproc.morphologyEx(temp, temp, Imgproc.MORPH_GRADIENT, element);
        Imgproc.threshold(result, result, 200, 255, Imgproc.THRESH_BINARY|Imgproc.THRESH_OTSU);

        return result;
    }

    public List<double[]> findOutsideBorders(Mat source){

        List<MatOfPoint> contours = new ArrayList<>();
        Mat hierarchy = new Mat();
        //轮廓查找，主要就是找最外表格框
        Imgproc.findContours(source, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
        hierarchy.release();

        // 找出轮廓对应凸包的四边形拟合
        List<MatOfPoint> squares = new ArrayList<>();
        List<MatOfPoint> hulls = new ArrayList<>();
        MatOfInt hull = new MatOfInt();
        MatOfPoint2f approx = new MatOfPoint2f();
        approx.convertTo(approx, CvType.CV_32F);
        for (MatOfPoint contour: contours) {
            // 边框的凸包
            Imgproc.convexHull(contour, hull);

            // 用凸包计算出新的轮廓点
            Point[] contourPoints = contour.toArray();
            int[] indices = hull.toArray();
            List<Point> newPoints = new ArrayList<>();
            for (int index : indices) {
                newPoints.add(contourPoints[index]);
            }
            MatOfPoint2f contourHull = new MatOfPoint2f();
            contourHull.fromList(newPoints);

            // 多边形拟合凸包边框(此时的拟合的精度较低)
            Imgproc.approxPolyDP(contourHull, approx, Imgproc.arcLength(contourHull, true)*0.02, true);

            // 筛选出面积大于某一阈值的，且四边形的各个角度都接近直角的凸四边形
            MatOfPoint approxf1 = new MatOfPoint();
            approx.convertTo(approxf1, CvType.CV_32S);

            //此处是筛选表格框，面积大于40000
            if (approx.rows() == 4 && Math.abs(Imgproc.contourArea(approx)) > 40000 &&
                    Imgproc.isContourConvex(approxf1)) {
                double maxCosine = 0;
                for (int j = 2; j < 5; j++) {
                    double cosine = Math.abs(OpenCVUtil.getAngle(approxf1.toArray()[j%4], approxf1.toArray()[j-2], approxf1.toArray()[j-1]));
                    maxCosine = Math.max(maxCosine, cosine);
                }
                // 考虑到图片倾斜等情况，角度大概72度
                if (maxCosine < 0.3) {
                    MatOfPoint tmp = new MatOfPoint();
                    contourHull.convertTo(tmp, CvType.CV_32S);
                    squares.add(approxf1);
                    hulls.add(tmp);
                }else {
                    approxf1.release();
                }
            }else {
                approxf1.release();
            }
            contourHull.release();
        }


        // 找出外接矩形最大的四边形
        int index = OpenCVUtil.findLargestSquare(squares);
        if (index == -1){
            throw new GlobalException("未找到答题卡！");
        }
        MatOfPoint largest_square = squares.get(index);
        if (largest_square.rows() == 0 || largest_square.cols() == 0)
            throw new GlobalException("未找到答题卡！");

        // 找到这个最大的四边形对应的凸边框，再次进行多边形拟合，此次精度较高，拟合的结果可能是大于4条边的多边形
        MatOfPoint contourHull = hulls.get(index);

        MatOfPoint2f tmp = new MatOfPoint2f();
        contourHull.convertTo(tmp, CvType.CV_32F);
        Imgproc.approxPolyDP(tmp, approx, 3, true);
        List<Point> newPointList = new ArrayList<>();
        double maxL = Imgproc.arcLength(approx, true) * 0.02;
        // 找到高精度拟合时得到的顶点中 距离小于低精度拟合得到的四个顶点maxL的顶点，排除部分顶点的干扰
        for (Point p : approx.toArray()) {
            if (!(OpenCVUtil.getSpacePointToPoint(p, largest_square.toList().get(0)) > maxL &&
                    OpenCVUtil.getSpacePointToPoint(p, largest_square.toList().get(1)) > maxL &&
                    OpenCVUtil.getSpacePointToPoint(p, largest_square.toList().get(2)) > maxL &&
                    OpenCVUtil.getSpacePointToPoint(p, largest_square.toList().get(3)) > maxL)) {
                newPointList.add(p);
            }
        }

//        // 找到剩余顶点连线中，边长大于 2 * maxL的四条边作为四边形物体的四条边
        List<double[]> lines = new ArrayList<>();
        for (int i = 0; i < newPointList.size(); i++) {
            Point p1 = newPointList.get(i);
            Point p2 = newPointList.get((i+1) % newPointList.size());
            if (OpenCVUtil.getSpacePointToPoint(p1, p2) > 2 * maxL) {
                lines.add(new double[]{p1.x, p1.y, p2.x, p2.y});
                //画出4条边线，真正识别过程中这些都是可以注释掉的，只是为了方便观察
//                Core.line(source, new Point(p1.x, p1.y), new Point( p2.x,  p2.y), new Scalar(255, 0, 0),4);
            }
        }

        squares.forEach(Mat::release);
        hulls.forEach(Mat::release);
        contours.forEach(Mat::release);
        hull.release();
        approx.release();
        tmp.release();
        return lines;
    }

    /**
     * 透视变换,并且剪裁出第一页答题卡
     * @return 得到1430*1800的图片
     */
    public Mat warpPerspective(Mat source,List<double[]> lines){
        List<Point> corners = new ArrayList<>();
        for (int i = 0; i < lines.size(); i++) {
            Point corner = OpenCVUtil.computeIntersect(lines.get(i),lines.get((i+1) % lines.size()));
            corners.add(corner);
        }
        OpenCVUtil.sortCorners(corners);

        // 计算目标图像的尺寸
//        Point p0 = corners.get(3);
//        Point p1 = corners.get(0);
//        Point p2 = corners.get(1);
//        Point p3 = corners.get(2);

        Point p0 = corners.get(0);
        Point p1 = corners.get(1);
        Point p2 = corners.get(2);
        Point p3 = corners.get(3);

        // 设置固定输出尺寸为1430*1800
        int fixedWidth = 1430;
        int fixedHeight = 1800;

        Mat result = Mat.zeros(fixedHeight, fixedWidth, CvType.CV_8UC3);

        MatOfPoint2f cornerMat = new MatOfPoint2f(p1, p2, p3, p0);
        MatOfPoint2f quadMat = new MatOfPoint2f(
                new Point(0, 0),
                new Point(fixedWidth, 0),
                new Point(fixedWidth, fixedHeight),
                new Point(0, fixedHeight)
        );

        // 提取图像，使用warpPerspective做图像的透视变换
        Mat transmtx = Imgproc.getPerspectiveTransform(cornerMat, quadMat);
        Imgproc.warpPerspective(source, result, transmtx, result.size());
        transmtx.release();
        cornerMat.release();
        quadMat.release();
        return result;
    }

    /**
     * 剪裁答题卡，获取最小的答题区域，去掉边框和标题之类的
     * @param source
     * @return
     */
    public Mat cutCard(Mat source){
        //设置剪切的边距，目的是裁剪表格边框，防止边框影响轮廓查找
        int cutTop = 108 * 2;
        int cutLeft = 5 * 2;
        int cutBottom = 250 * 2;
        int cutRight = 30 * 2;
        return source.submat(cutTop,source.rows()-cutTop - cutBottom,cutLeft,source.cols()-cutLeft - cutRight);
    }

    /**
     * 通过关键点鉴别 答题卡涂写识别
     */
    public CardRecognitionResult cardResult(Mat source,Mat threshold,Integer maxQuestionCount){

        Mat resultImage = source.clone();
        List<MatOfPoint> answerList = new ArrayList<>();
        List<MatOfPoint> contours = new ArrayList<>();
        Imgproc.findContours(threshold, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

        for (MatOfPoint mop : contours) {
            //每个轮廓给出外包矩形，便于操作 ,
            // TODO: 这里需要识别涂写的比例
            Rect rect = Imgproc.boundingRect(mop);
            if (rect.width > 30 && rect.height > 20 ) {
                answerList.add(mop);
            }
        }

        // 生成所有点位的循环
        CardRecognitionResult cardRecognitionResult = new CardRecognitionResult();
        for (int question = 0; question < maxQuestionCount; question++) {
            CardRecognitionResult.Result questionResult = new CardRecognitionResult.Result();
            questionResult.setNo(question + 1);
            cardRecognitionResult.getResultList().add(questionResult);
            String result = "";
            for (int option = 0; option < 4; option++) {
                // 计算坐标 ，这些数字都是测算出来的，每个涂写区域中获取一个点，看是否在矩形内
                int x = 40 + 50 * (question % 5 ) + ((question % 20 / 5) * 360);
                int y = 67 + 40 * option + (210 * (question / 20));
                // 创建点
                Point point = new Point(x, y);
                Optional<MatOfPoint> findPoint = answerList.stream().filter(mop -> {
                    Rect rect = Imgproc.boundingRect(mop);
                    return rect.contains(point);
                }).findAny();
                if(findPoint.isPresent()){
                    result += (char)(option + 65);
                    // 在结果图像上绘制矩形框
                    Rect rect = Imgproc.boundingRect(findPoint.get());

                    // 将坐标转换到原始图像坐标系
                    Point topLeft = new Point(rect.x, rect.y);
                    Point bottomRight = new Point(rect.x + rect.width, rect.y + rect.height);
                    Core.rectangle(resultImage, topLeft, bottomRight, new Scalar(0, 255, 0), 2);
                }else {
//                    log.info("{}未选择[{},{}]",question + 1,x,y);
                }
            }
            questionResult.setResult(result);
        }

        MatOfByte matOfByte = new MatOfByte();
        Highgui.imencode(".png", resultImage, matOfByte);
        cardRecognitionResult.setResultImageArray(matOfByte.toArray());
        matOfByte.release();
        resultImage.release();
        answerList.forEach(Mat::release);
        contours.forEach(Mat::release);
        //41题识别出错
//        String r  ="412323241323412231342314223142314232314231241314232314223142231422323433333";
        return cardRecognitionResult;
    }

    /**
     * 通过选项区域掩膜 答题卡涂写识别
     * 错误率比涂写识别高。现阶段不是很准
     * @return
     */
    public CardRecognitionResult cardResult2(Mat source,Mat threshold,Integer maxQuestionCount){
        Mat resultImage = source.clone();

        // 生成所有点位的循环
        CardRecognitionResult cardRecognitionResult = new CardRecognitionResult();
        for (int question = 0; question < maxQuestionCount; question++) {
            CardRecognitionResult.Result questionResult = new CardRecognitionResult.Result();
            questionResult.setNo(question + 1);
            cardRecognitionResult.getResultList().add(questionResult);
            String result = "";
            for (int option = 0; option < 4; option++) {
                // 计算坐标 ，这些数字都是测算出来的，每个涂写区域中获取一个点，看是否在矩形内
                int x = 20 + 50 * (question % 5 ) + ((question % 20 / 5) * 360);
                int y = 57 + 40 * option + (210 * (question / 20));
                int width = 34;
                int height = 20;
                Point topLeft = new Point(x, y);
                Point bottomRight = new Point(x + width, y + height);
                Core.rectangle(resultImage, topLeft, bottomRight, new Scalar(0, 255, 0), 2);

                // 创建点
//                Point point = new Point(x, y);
//                Optional<MatOfPoint> findPoint = answerList.stream().filter(mop -> {
//                    Rect rect = Imgproc.boundingRect(mop);
//                    return rect.contains(point);
//                }).findAny();
//                if(findPoint.isPresent()){
//                    result += (char)(option + 65);
//                    // 在结果图像上绘制矩形框
//                    Rect rect = Imgproc.boundingRect(findPoint.get());
//
//                    // 将坐标转换到原始图像坐标系
//                    Point topLeft = new Point(rect.x, rect.y);
//                    Point bottomRight = new Point(rect.x + rect.width, rect.y + rect.height);
//                    Core.rectangle(resultImage, topLeft, bottomRight, new Scalar(0, 255, 0), 2);
//                }else {
////                    log.info("{}未选择[{},{}]",question + 1,x,y);
//                }
            }
            questionResult.setResult(result);
        }

        MatOfByte matOfByte = new MatOfByte();
        Highgui.imencode(".png", resultImage, matOfByte);
        cardRecognitionResult.setResultImageArray(matOfByte.toArray());
//        matOfByte.release();
//        resultImage.release();
//        answerList.forEach(Mat::release);
//        contours.forEach(Mat::release);
//        //41题识别出错
//        String r  ="412323241323412231342314223142314232314231241314232314223142231422323433333";
        return cardRecognitionResult;
    }
}
